1
0
forked from osmarks/potatOS

Compare commits

11 Commits

Author SHA1 Message Date
osmarks
af603c81b7 some fixes for nesting potatOSes 2025-07-15 11:58:44 +01:00
osmarks
ce96a1fa38 Remove typo 2025-03-10 13:57:17 +00:00
osmarks
06a4fb6145 fix devfs a bit 2025-03-10 13:54:32 +00:00
423f622271 DevFS prototype 2024-09-03 13:48:53 +01:00
d3a43eab28 Fix VFS list bug 2024-09-03 12:52:38 +01:00
3542ab25c1 Update docs accordingly 2024-09-03 12:41:39 +01:00
2453c7756a fix drive mounts, VFS listings(ish) 2024-09-03 12:31:57 +01:00
288ef5f03a cleaner VFS abstraction 2024-09-03 12:00:47 +01:00
4cbe8f81d3 Merge branch 'master' of https://git.osmarks.net/osmarks/potatos 2024-09-03 10:34:54 +01:00
233bd28aab Tweak programs, filesystem, overlays 2024-09-03 10:34:52 +01:00
88469da2cb Merge pull request 'Add yet another sandbox escape' (#9) from 6_4/potatOS:master into master
Reviewed-on: osmarks/potatOS#9
2024-07-16 14:17:27 +00:00
13 changed files with 544 additions and 288 deletions

View File

@@ -17,6 +17,8 @@ Thanks to technology, we're able to offer a live PotatOS instance in your browse
<button id="launch-demo">Experience PotatOS</button> <button id="launch-demo">Experience PotatOS</button>
<div id="computer"></div> <div id="computer"></div>
(if the emulator gets stuck, please refresh this page)
<noscript> <noscript>
Experiencing PotatOS requires JavaScript. Please enable it. Experiencing PotatOS requires JavaScript. Please enable it.
</noscript> </noscript>
@@ -72,7 +74,7 @@ Unlike most "OS"es for CC (primarily excluding Opus OS, which is actually useful
- To ease large-scale network management, PotatOS's networking daemon turns on any networked potatOS computers. - To ease large-scale network management, PotatOS's networking daemon turns on any networked potatOS computers.
- Improves connected signs, if Plethora Peripherals is installed. - Improves connected signs, if Plethora Peripherals is installed.
- Recycle bin capability stops accidental loss of files. - Recycle bin capability stops accidental loss of files.
- `exorcise` command, which is like delete but better. - `exorcise` command, which is like `delete` but better.
- Support for a wide variety of Lorem Ipsum integrated into the OS. - Support for a wide variety of Lorem Ipsum integrated into the OS.
- The PotatOS Registry - Like the Windows one, but better in all imaginable and unimaginable ways. Edit and view its contents with the `est` command. - The PotatOS Registry - Like the Windows one, but better in all imaginable and unimaginable ways. Edit and view its contents with the `est` command.
- Window manager shipped. I forgot what it is and how to use it. - Window manager shipped. I forgot what it is and how to use it.
@@ -93,6 +95,7 @@ Unlike most "OS"es for CC (primarily excluding Opus OS, which is actually useful
- Live threat updates using our advanced algorithms. - Live threat updates using our advanced algorithms.
- PotatOS Epenthesis' rewritten security model fixes many exploits and adds others while reducing boot times. - PotatOS Epenthesis' rewritten security model fixes many exploits and adds others while reducing boot times.
- IPC mechanism. - IPC mechanism.
- Virtual filesystems abstraction.
## Architecture ## Architecture

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,60 @@
local prefixes = {
{-12, "p"},
{-9, "n"},
{-6, "u"},
{-3, "m"},
{0, ""},
{3, "k"},
{6, "M"}
}
local function SI_prefix(value, unit)
local x = math.log(value, 10)
local last
for _, t in ipairs(prefixes) do
if t[1] > x then
break
end
last = t
end
local dp = 2 - math.floor(x - last[1])
return (("%%.%df%%s%%s"):format(dp)):format(value / 10^(last[1]), last[2], unit)
end
local w = term.getSize()
local rows = {}
for _, info in pairs(process.list()) do for _, info in pairs(process.list()) do
textutils.pagedPrint(("%s %f %f"):format(info.name or info.ID, info.execution_time, info.ctime)) table.insert(rows, { info.name or tostring(info.ID), SI_prefix(info.execution_time, "s"), SI_prefix(info.ctime, "s") })
end
local max_width_per_column = {}
for _, row in ipairs(rows) do
for i, cell in ipairs(row) do
max_width_per_column[i] = math.max(max_width_per_column[i] or 0, cell:len() + 1)
end
end
local vw_width = 0
for i = #max_width_per_column, 1, -1 do
if i > 1 then
vw_width = vw_width + max_width_per_column[i]
end
end
local fw_start = w - vw_width
for _, row in ipairs(rows) do
local s
for i, cell in ipairs(row) do
if i == 1 then
s = cell:sub(1, fw_start - 1) .. (" "):rep((fw_start - 1) - cell:len())
else
cell = " " .. cell
s = s .. (" "):rep(max_width_per_column[i] - cell:len()) .. cell
end
end
textutils.pagedPrint(s)
end end

128
src/bin/devfs.lua Normal file
View File

@@ -0,0 +1,128 @@
local vfs = {}
local getters = {"is", "has", "get"}
local setters = {"set"}
function vfs.list(path)
local segs = fs.segment(path)
if #segs == 0 then
return peripheral.getNames()
elseif #segs == 1 then
local methods = peripheral.getMethods(segs[1])
local out = {}
for _, v in pairs(methods) do
local set
for _, s in pairs(setters) do
local mat = v:match("^" .. s .. "([A-Z].+)")
if mat then
set = mat
break
end
end
local get
for _, g in pairs(getters) do
local mat = v:match("^" .. g .. "([A-Z].+)")
if mat then
get = mat
break
end
end
if get then table.insert(out, get)
elseif set then table.insert(out, set)
else table.insert(out, v) end
end
return out
elseif #segs == 2 then
end
end
local function write_handle(callback)
local buffer = ""
local r_write_handle = {}
function r_write_handle.write(text)
buffer = buffer .. text
end
function r_write_handle.close()
callback(buffer)
end
function r_write_handle.flush() end
function r_write_handle.writeLine(text) r_write_handle.write(text) r_write_handle.write("\n") end
return r_write_handle
end
local call_results = {}
function vfs.open(path, mode)
local segs = fs.segment(path)
if #segs == 2 and segs[2]:match "^[A-Z]" then -- getter/setter configuration
if mode:match "w" then
return write_handle(function(buffer)
local ok, res
for _, s in pairs(setters) do
local ok2, res2 = pcall(peripheral.call, segs[1], s .. segs[2], textutils.unserialise(buffer))
ok = ok or ok2
res = res or res2
end
if not ok then error(res) end
end)
else
-- TODO multiple returns
local result
for _, g in pairs(getters) do
local ok, res = pcall(peripheral.call, segs[1], g .. segs[2])
result = result or (ok and res)
end
local text = textutils.serialise(result)
return fs._make_handle(text)
end
elseif #segs == 2 then
if mode:match "^w" then
return write_handle(function(buffer)
call_results[fs.combine(path, "")] = peripheral.call(segs[1], segs[2], unpack(textutils.unserialise(buffer)))
end)
end
if mode:match "^r" then
local rp = fs.combine(path, "")
local h
if call_results[rp] then h = fs._make_handle(textutils.serialise(call_results[rp]))
else h = fs._make_handle("") end
call_results[rp] = nil
return h
end
error "invalid IO mode"
end
end
function vfs.exists(path)
local segs = fs.segment(path)
if #segs == 0 then
return true
else
return peripheral.getType(segs[1]) ~= nil
end
end
function vfs.isReadOnly(path)
local segs = fs.segment(path)
if #segs == 2 and segs[2]:match "^[A-Z]" then -- getter/setter configuration
local methods = peripheral.getMethods(segs[1])
for _, s in pairs(setters) do
for _, m in pairs(methods) do
if m == s .. segs[2] then
return false
end
end
end
return true
end
return false
end
function vfs.isDir(path)
local segs = fs.segment(path)
return #segs <= 1
end
function vfs.getDrive(path) return "devfs" end
fs.mountVFS(shell.resolve(...), vfs)

View File

@@ -24,7 +24,7 @@ print()
print(snd) print(snd)
print() print()
if arg == "headless" then if arg == "headless" then
ccemux.echo "ready" if ccemux then ccemux.echo "ready" end
while true do coroutine.yield() end while true do coroutine.yield() end
else else
print "Press a key to continue..." print "Press a key to continue..."

1
src/bin/umount.lua Normal file
View File

@@ -0,0 +1 @@
fs.unmountVFS(shell.resolve(...))

View File

@@ -1,6 +1,6 @@
-- thanks to valued user 6_4 for the suggestion -- thanks to valued user 6_4 for the suggestion
local function different_to_global(candidate_fs) local function different_from_global(candidate_fs)
local seen = {} local seen = {}
for _, i in pairs(fs.list "") do for _, i in pairs(fs.list "") do
seen[i] = true seen[i] = true
@@ -19,7 +19,7 @@ local function is_probably_filesystem(x)
for _, k in pairs(keys) do for _, k in pairs(keys) do
if type(x[k]) ~= "function" then return false end if type(x[k]) ~= "function" then return false end
end end
return different_to_global(x) return different_from_global(x)
end end
local function harvest_upvalues(fn) local function harvest_upvalues(fn)
@@ -110,7 +110,7 @@ local escapes = {
i = i + 1 i = i + 1
end end
end, end,
scan_most_threads = function() scan_most_threads = function()
if not debug then return end if not debug then return end
if not (debug.getinfo and debug.getlocal) then return end if not (debug.getinfo and debug.getlocal) then return end
local running = coroutine.running() local running = coroutine.running()
@@ -142,4 +142,4 @@ return function()
return err return err
end end
end end
end end

View File

@@ -13,6 +13,11 @@ end
function sandboxlib.dispatch_if_restricted(rkey, original, restricted) function sandboxlib.dispatch_if_restricted(rkey, original, restricted)
local out = {} local out = {}
for k, v in pairs(restricted) do
if not original[k] then
out[k] = v
end
end
for k, v in pairs(original) do for k, v in pairs(original) do
out[k] = function(...) out[k] = function(...)
if processrestriction(rkey) then if processrestriction(rkey) then

View File

@@ -11,21 +11,6 @@ local function copy(tabl)
return new return new
end end
-- Deep-map all values in a table
local function deepmap(table, f, path)
local path = path or ""
local new = {}
for k, v in pairs(table) do
local thisp = path .. "." .. k
if type(v) == "table" and v ~= table then -- bodge it to not stackoverflow
new[k] = deepmap(v, f, thisp)
else
new[k] = f(v, k, thisp)
end
end
return new
end
-- Takes a list of keys to copy, returns a function which takes a table and copies the given keys to a new table -- Takes a list of keys to copy, returns a function which takes a table and copies the given keys to a new table
local function copy_some_keys(keys) local function copy_some_keys(keys)
return function(from) return function(from)
@@ -41,13 +26,6 @@ local function copy_some_keys(keys)
end end
end end
-- Simple string operations
local function starts_with(s, with)
return string.sub(s, 1, #with) == with
end
local function ends_with(s, with)
return string.sub(s, -#with, -1) == with
end
local function contains(s, subs) local function contains(s, subs)
return string.find(s, subs) ~= nil return string.find(s, subs) ~= nil
end end
@@ -77,56 +55,18 @@ local function add_to_table(t1, t2)
end end
end end
local fscombine, fsgetname, fsgetdir = fs.combine, fs.getName, fs.getDir
-- Convert path to canonical form -- Convert path to canonical form
local function canonicalize(path) local function canonicalize(path)
return fs.combine(path, "") return fscombine(path, "")
end
-- Checks whether a path is in a directory
local function path_in(p, dir)
return starts_with(canonicalize(p), canonicalize(dir))
end
local function make_mappings(root)
return {
["/disk"] = "/disk",
["/rom"] = "/rom",
default = root
}
end
local function get_root(path, mappings)
for mapfrom, mapto in pairs(mappings) do
if path_in(path, mapfrom) then
return mapto, mapfrom
end
end
return mappings.default, "/"
end
-- Escapes lua patterns in a string. Should not be needed, but lua is stupid so the only string.replace thing is gsub
local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])'
local function escape(str)
return str:gsub(quotepattern, "%%%1")
end
local function strip(p, root)
return p:gsub("^" .. escape(canonicalize(root)), "")
end
local function resolve_path(path, mappings)
local root, to_strip = get_root(path, mappings)
local newpath = strip(fs.combine(root, path), to_strip)
if path_in(newpath, root) then return newpath end
return resolve_path(newpath, mappings)
end end
local function segments(path) local function segments(path)
local segs, rest = {}, canonicalize(path) local segs, rest = {}, canonicalize(path)
if rest == "" then return {} end -- otherwise we'd get "root" and ".." for some broken reason if rest == "" then return {} end -- otherwise we'd get "root" and ".." for some broken reason
repeat repeat
table.insert(segs, 1, fs.getName(rest)) table.insert(segs, 1, fsgetname(rest))
rest = fs.getDir(rest) rest = fsgetdir(rest)
until rest == "" until rest == ""
return segs return segs
end end
@@ -134,18 +74,18 @@ end
local function combine(segs) local function combine(segs)
local out = "" local out = ""
for _, p in pairs(segs) do for _, p in pairs(segs) do
out = fs.combine(out, p) out = fscombine(out, p)
end end
return out return out
end end
-- Fetch the contents of URL "u" local vfs_defaults = {}
local function fetch(u)
local h = http.get(u) function vfs_defaults.getSize(path) return 0 end
local c = h.readAll() function vfs_defaults.getFreeSpace(path) return 0 end
h.close() function vfs_defaults.makeDir(path) error "Access denied" end
return c function vfs_defaults.delete(path) error "Access denied" end
end function vfs_defaults.isReadOnly(path) return true end
-- Make a read handle for a string -- Make a read handle for a string
-- PS#8FE487EF: Incompletely implemented handle behaviour lead to strange bugs on recent CC -- PS#8FE487EF: Incompletely implemented handle behaviour lead to strange bugs on recent CC
@@ -172,105 +112,176 @@ local function make_handle(text)
return h return h
end end
-- Get a path from a filesystem overlay -- make virtual filesystem from files (no nested directories for simplicity)
local function path_in_overlay(overlay, path) local function vfs_from_files(files)
return overlay[canonicalize(path)] return {
list = function(path)
if path ~= "" then return {} end
local out = {}
for k, v in pairs(files) do
table.insert(out, k)
end
return out
end,
open = function(path, mode)
return make_handle(files[path])
end,
exists = function(path)
return files[path] ~= nil or path == ""
end,
isReadOnly = function(path)
return true
end,
isDir = function(path)
if path == "" then return true end
return false
end,
getDrive = function(_) return "memory" end,
getSize = function(path)
return #files[path]
end,
getFreeSpace = function() return 0 end,
makeDir = function() end,
delete = function() end,
move = function() end,
copy = function() end,
attributes = function(path)
return {
size = #files[path],
modification = os.epoch "utc"
}
end
}
end end
local this_level_env = _G local function create_FS(vfstree)
-- Create a modified FS table which confines you to root and has some extra read-only pseudofiles.
local function create_FS(root, overlay)
local fs = fs local fs = fs
local mappings = make_mappings(root)
local vfstree = { local function is_usable_node(node)
mount = "potatOS", return node.mount or node.vfs
children = { end
["disk"] = { mount = "disk" },
["rom"] = { mount = "rom" },
--["virtual_test"] = { virtual = "bees" }
}
}
local function resolve(sandbox_path) local function resolve(sandbox_path, ignore_usability)
local segs = segments(sandbox_path) local segs = segments(sandbox_path)
local current_tree = vfstree local current_tree = vfstree
local last_usable_node, last_segs = nil, nil
while true do while true do
if is_usable_node(current_tree) then
last_usable_node = current_tree
last_segs = copy(segs)
end
local seg = segs[1] local seg = segs[1]
if current_tree.children and current_tree.children[seg] then if seg and current_tree.children and current_tree.children[seg] then
table.remove(segs, 1) table.remove(segs, 1)
current_tree = current_tree.children[seg] current_tree = current_tree.children[seg]
else break end else break end
end end
if ignore_usability then return current_tree, segs end
return last_usable_node, last_segs
end end
local new_overlay = {} local function resolve_node_segs(node, segs)
for k, v in pairs(overlay) do if node.mount then return fs, fscombine(node.mount, combine(segs)) end
new_overlay[canonicalize(k)] = v return node.vfs, combine(segs)
end
local function resolve_path(sandbox_path)
local node, segs = resolve(sandbox_path)
return resolve_node_segs(node, segs)
end end
local function lift_to_sandbox(f, n) local function lift_to_sandbox(f, n)
return function(...) return function(path)
local args = map(function(x) return resolve_path(x, mappings) end, {...}) local vfs, path = resolve_path(path)
return f(table.unpack(args)) return (vfs[n] or vfs_defaults[n])(path)
end end
end end
local new = copy_some_keys {"getDir", "getName", "combine", "complete"} (fs) local new = copy_some_keys {"getDir", "getName", "combine", "complete"} (fs)
function new.isReadOnly(path)
return path_in_overlay(new_overlay, path) or starts_with(canonicalize(path), "rom")
end
function new.open(path, mode) function new.open(path, mode)
if (contains(mode, "w") or contains(mode, "a")) and new.isReadOnly(path) then if (contains(mode, "w") or contains(mode, "a")) and new.isReadOnly(path) then
error "Access denied" error "Access denied"
else else
local overlay_data = path_in_overlay(new_overlay, path) local vfs, path = resolve_path(path)
if overlay_data then return vfs.open(path, mode)
if type(overlay_data) == "function" then overlay_data = overlay_data(this_level_env) end
return make_handle(overlay_data), "YAFSS overlay"
end
return fs.open(resolve_path(path, mappings), mode)
end end
end end
function new.exists(path) function new.move(src, dest)
if path_in_overlay(new_overlay, path) ~= nil then return true end local src_vfs, src_path = resolve_path(src)
return fs.exists(resolve_path(path, mappings)) local dest_vfs, dest_path = resolve_path(dest)
end if src_vfs == dest_vfs then
return src_vfs.move(src_path, dest_path)
function new.overlay()
return map(function(x)
if type(x) == "function" then return x(this_level_env)
else return x end
end, new_overlay)
end
function new.list(dir)
local sdir = canonicalize(resolve_path(dir, mappings))
local ocontents = {}
for opath in pairs(new_overlay) do
if fs.getDir(opath) == sdir then
table.insert(ocontents, fs.getName(opath))
end
end
local ok, contents = pcall(fs.list, sdir)
-- in case of error (nonexistent dir, probably) return overlay contents
-- very awful temporary hack until I can get a nicer treeized VFS done
if not ok then
if #ocontents > 0 then return ocontents end
error(contents)
else else
for _, v in pairs(ocontents) do if src_vfs.isReadOnly(src_path) then error "Access denied" end
table.insert(contents, v) new.copy(src, dest)
end src_vfs.delete(src_path)
return contents
end end
end end
add_to_table(new, map(lift_to_sandbox, copy_some_keys {"isDir", "getDrive", "getSize", "getFreeSpace", "makeDir", "move", "copy", "delete", "isDriveRoot"} (fs))) function new.copy(src, dest)
local src_vfs, src_path = resolve_path(src)
local dest_vfs, dest_path = resolve_path(dest)
if src_vfs == dest_vfs then
return src_vfs.copy(src_path, dest_path)
else
if src_vfs.isDir(src_path) then
dest_vfs.makeDir(dest_path)
for _, f in pairs(src_vfs.list(src_path)) do
new.copy(fscombine(src, f), fscombine(dest, f))
end
else
local dest_fh = dest_vfs.open(dest_path, "wb")
local src_fh = src_vfs.open(src_path, "rb")
dest_fh.write(src_fh.readAll())
src_fh.close()
dest_fh.close()
end
end
end
function new.mountVFS(path, vfs)
local node, relpath = resolve(path)
while #relpath > 0 do
local seg = table.remove(relpath, 1)
if not node.children then node.children = {} end
if not node.children[seg] then node.children[seg] = {} end
node = node.children[seg]
end
node.vfs = vfs
end
function new.unmountVFS(path)
local node, relpath = resolve(path)
if #relpath == 0 then
node.vfs = nil
else
error "Not a mountpoint"
end
end
function new.list(path)
local node, segs = resolve(path, true)
local vfs, path = resolve_path(path)
if #segs > 0 then return vfs.list(path) end
local out = {}
local seen = {}
for k, v in pairs(node.children or {}) do
table.insert(out, k)
seen[k] = true
end
for _, v in pairs(vfs.list(path)) do
if not seen[v] then
table.insert(out, v)
end
end
return out
end
add_to_table(new, map(lift_to_sandbox, copy_some_keys {"isDir", "getDrive", "getSize", "getFreeSpace", "makeDir", "delete", "isDriveRoot", "exists", "isReadOnly", "attributes"} (fs)))
function new.find(wildcard) function new.find(wildcard)
local function recurse_spec(results, path, spec) -- From here: https://github.com/Sorroko/cclite/blob/62677542ed63bd4db212f83da1357cb953e82ce3/src/emulator/native_api.lua local function recurse_spec(results, path, spec) -- From here: https://github.com/Sorroko/cclite/blob/62677542ed63bd4db212f83da1357cb953e82ce3/src/emulator/native_api.lua
@@ -310,7 +321,7 @@ local function create_FS(root, overlay)
to_add.c = new.dump(path) to_add.c = new.dump(path)
to_add.t = "d" to_add.t = "d"
else else
local fh = new.open(path, "r") local fh = new.open(path, "rb")
to_add.c = fh.readAll() to_add.c = fh.readAll()
fh.close() fh.close()
end end
@@ -327,13 +338,16 @@ local function create_FS(root, overlay)
new.makeDir(path) new.makeDir(path)
new.load(f.c, path) new.load(f.c, path)
else else
local fh = new.open(path, "w") local fh = new.open(path, "wb")
fh.write(f.c) fh.write(f.c)
fh.close() fh.close()
end end
end end
end end
new.segment = segments
new._make_handle = make_handle
return new return new
end end
@@ -405,30 +419,11 @@ local gf, sf = getfenv, setfenv
-- a map of paths to either strings containing their contents or functions returning them -- a map of paths to either strings containing their contents or functions returning them
-- and a table of extra APIs and partial overrides for existing APIs -- and a table of extra APIs and partial overrides for existing APIs
local function make_environment(API_overrides, current_process) local function make_environment(API_overrides, current_process)
local env_host = string.format("YAFSS on %s", _HOST)
local environment = copy_some_keys(allowed_APIs)(_G) local environment = copy_some_keys(allowed_APIs)(_G)
-- if function is not from within the VM, return env from within sandbox
function environment.getfenv(arg)
local env
if type(arg) == "number" then return gf() end
if not env or type(env._HOST) ~= "string" or not env._HOST == env_host then
return gf()
else
return env
end
end
--[[ -- I sure hope this doesn't readd the security issues!
Fix PS#AD2A532C environment.getfenv = gf
Allowing `setfenv` to operate on any function meant that privileged code could in some cases be manipulated to leak information or operate undesirably. Due to this, we restrict it, similarly to getfenv. environment.setfenv = sf
]]
function environment.setfenv(fn, env)
local nenv = gf(fn)
if not nenv or type(nenv._HOST) ~= "string" or not nenv._HOST == env_host then
return false
end
return sf(fn, env)
end
local load = load local load = load
function environment.load(code, file, mode, env) function environment.load(code, file, mode, env)
@@ -437,7 +432,6 @@ Allowing `setfenv` to operate on any function meant that privileged code could i
environment._G = environment environment._G = environment
environment._ENV = environment environment._ENV = environment
environment._HOST = env_host
function environment.os.shutdown() function environment.os.shutdown()
process.IPC(current_process, "power_state", "shutdown") process.IPC(current_process, "power_state", "shutdown")
@@ -485,4 +479,4 @@ local function run(API_overrides, init, logger)
end end
end end
return { run = run, create_FS = create_FS } return { run = run, create_FS = create_FS, vfs_from_files = vfs_from_files }

View File

@@ -35,9 +35,9 @@ end
term.setCursorBlink(false) term.setCursorBlink(false)
print "Loading..." print "Loading..."
if settings.get "potatOS.rph_mode" == true then if settings.get "potatOS.rph_mode" == true then
print "PotatOS Rph Compliance Mode: Enabled." print "PotatOS Rph Compliance Mode: Enabled."
return false return false
end end
require "stack_trace" require "stack_trace"
@@ -47,8 +47,7 @@ local registry = require "registry"
--[[ --[[
Server Policy Framework Server Policy Framework
On 12/01/2020 CE (this is probably overprecise and I doubt anyone will care, yes), there was a weird incident on SwitchCraft involving some turtles somehow getting potatOS installed (seriously, "somehow" is accurate, I have no idea what caused this and attempted to uninstall it when someone actually pinged me; I think it involved a turtle getting set to ID 0 somehow, who knows how potatOS got onto ID 0 in the first place). In light of this (and it apparently breaking rule 9, despite this not having any involvement from me except for me remotely uninstalling it), SC's admins have demanded some features be disabled (EZCopy). On 12/01/2020 CE (this is probably overprecise and I doubt anyone will care, yes), the ID 0 bug resulted in potatOS improving a large quantity of turtles, apparently causing problems. In light of this, SC's admins have demanded some features be disabled (EZCopy). This is a reasonably generic way to handle server-specific special cases.
Since I don't really want to hardcode random SwitchCraft APIs deep in the code somewhere (it's worrying that they *have* specific ones, as it seems like some programs are being written specifically against them now - seems kind of EEE), and other people will inevitably demand their own special cases, I'm making what should be a reasonably generic way to handle this.
]] ]]
local SPF = { local SPF = {
@@ -98,7 +97,7 @@ local function rot13(s)
end end
end end
return table.concat(out) return table.concat(out)
end end
local debugtraceback = debug and debug.traceback local debugtraceback = debug and debug.traceback
local logfile = fs.open("latest.log", "a") local logfile = fs.open("latest.log", "a")
@@ -255,7 +254,7 @@ local function clear_space(reqd)
if fs.getFreeSpace "/" > (reqd + 4096) then if fs.getFreeSpace "/" > (reqd + 4096) then
return return
end end
for _, file in pairs(fs.find(i)) do for _, file in pairs(fs.find(i)) do
print("Deleting", file) print("Deleting", file)
fs.delete(file) fs.delete(file)
@@ -307,7 +306,7 @@ local function fread(n)
out = {string.char(out)} out = {string.char(out)}
while true do while true do
local next = f.read() local next = f.read()
if not next then if not next then
out = table.concat(out) out = table.concat(out)
break break
end end
@@ -452,16 +451,16 @@ function _G.report_incident(incident, flags, options)
for k, v in pairs(options.extra_meta) do hostdata[k] = v end for k, v in pairs(options.extra_meta) do hostdata[k] = v end
end end
if type(incident) ~= "string" then error "incident description must be string" end if type(incident) ~= "string" then error "incident description must be string" end
local payload = json.encode { local payload = json.encode {
report = incident, report = incident,
host = hostdata, host = hostdata,
code = options.code or last_loaded, code = options.code or last_loaded,
flags = flags flags = flags
} }
-- Workaround craftos-pc bug by explicitly specifying Content-Length header -- Workaround craftos-pc bug by explicitly specifying Content-Length header
http.request { http.request {
url = "https://spudnet.osmarks.net/report", url = "https://spudnet.osmarks.net/report",
body = payload, body = payload,
headers = { headers = {
["content-type"] = "application/json", ["content-type"] = "application/json",
-- Workaround for CraftOS-PC bug where it apparently sends 0, which causes problems in the backend -- Workaround for CraftOS-PC bug where it apparently sends 0, which causes problems in the backend
@@ -471,7 +470,7 @@ function _G.report_incident(incident, flags, options)
} }
add_log("reported an incident %s", incident) add_log("reported an incident %s", incident)
end end
local disk_code_template = [[ local disk_code_template = [[
settings.set("potatOS.gen_count", %d) settings.set("potatOS.gen_count", %d)
settings.set("potatOS.ancestry", %s) settings.set("potatOS.ancestry", %s)
@@ -493,7 +492,7 @@ local function generate_disk_code()
("wget %q startup"):format((registry.get "potatOS.current_manifest.base_URL" or manifest:gsub("/manifest$", "")) .. "/autorun.lua") ("wget %q startup"):format((registry.get "potatOS.current_manifest.base_URL" or manifest:gsub("/manifest$", "")) .. "/autorun.lua")
) )
end end
-- Upgrade other disks to contain potatOS and/or load debug programs (mostly the "OmniDisk") off them. -- Upgrade other disks to contain potatOS and/or load debug programs (mostly the "OmniDisk") off them.
local function process_disk(disk_side) local function process_disk(disk_side)
local mp = disk.getMountPath(disk_side) local mp = disk.getMountPath(disk_side)
@@ -503,8 +502,8 @@ local function process_disk(disk_side)
local sig_file = fs.combine(mp, "signature") local sig_file = fs.combine(mp, "signature")
-- shell.run disks marked with the Brand of PotatOS -- shell.run disks marked with the Brand of PotatOS
-- except not actually, it's cool and uses load now -- except not actually, it's cool and uses load now
if fs.exists(ds) and fs.exists(sig_file) then if fs.exists(ds) and fs.exists(sig_file) then
local code = fread(ds) local code = fread(ds)
local sig_raw = fread(sig_file) local sig_raw = fread(sig_file)
local sig local sig
@@ -538,10 +537,10 @@ local function process_disk(disk_side)
else else
printError "Invalid Signature!" printError "Invalid Signature!"
printError "Initiating Procedure 5." printError "Initiating Procedure 5."
report_incident("invalid signature on disk", report_incident("invalid signature on disk",
{"security", "disk_signature"}, {"security", "disk_signature"},
{ {
code = code, code = code,
extra_meta = { signature = sig_raw, disk_ID = disk_ID, disk_side = disk_side, mount_path = mp } extra_meta = { signature = sig_raw, disk_ID = disk_ID, disk_side = disk_side, mount_path = mp }
}) })
printError "This incident has been reported." printError "This incident has been reported."
@@ -562,12 +561,12 @@ local function disk_handler()
-- Detect disks initially -- Detect disks initially
for _, n in pairs(peripheral.getNames()) do for _, n in pairs(peripheral.getNames()) do
-- lazily avoid crashing, this is totally fine and not going to cause problems -- lazily avoid crashing, this is totally fine and not going to cause problems
if peripheral.getType(n) == "drive" then if peripheral.getType(n) == "drive" then
local ok, err = pcall(process_disk, n) local ok, err = pcall(process_disk, n)
if not ok then printError(err) end if not ok then printError(err) end
end end
end end
-- Detect disks as they're put in. Mwahahahaha. -- Detect disks as they're put in. Mwahahahaha.
-- Please note that this is for definitely non-evil purposes only. -- Please note that this is for definitely non-evil purposes only.
while true do while true do
@@ -576,7 +575,7 @@ local function disk_handler()
if not ok then printError(err) end if not ok then printError(err) end
end end
end end
--[[ --[[
Fix bug PS#201CA2AA Fix bug PS#201CA2AA
Serializing functions, recursive tables, etc. - this is done fairly often - can cause a complete crash of the SPUDNET process. This fixes that. Serializing functions, recursive tables, etc. - this is done fairly often - can cause a complete crash of the SPUDNET process. This fixes that.
@@ -645,9 +644,10 @@ local function websocket_remote_debugging()
local function connect() local function connect()
if ws then ws.close() end if ws then ws.close() end
ws, err = http.websocket "wss://spudnet.osmarks.net/v4?enc=json" local url = "wss://spudnet.osmarks.net/v4?enc=json&rand=" .. math.random(0, 0xFFFFFFF)
ws, err = http.websocket(url)
if not ws then add_log("websocket failure %s", err) return false end if not ws then add_log("websocket failure %s", err) return false end
ws.url = "wss://spudnet.osmarks.net/v4?enc=json" ws.url = url
send_packet { type = "identify", request_ip = true, implementation = string.format("PotatOS %s on %s", (settings.get "potatOS.current_hash" or "???"):sub(1, 8), _HOST) } send_packet { type = "identify", request_ip = true, implementation = string.format("PotatOS %s on %s", (settings.get "potatOS.current_hash" or "???"):sub(1, 8), _HOST) }
send_packet { type = "set_channels", channels = { "client:potatOS" } } send_packet { type = "set_channels", channels = { "client:potatOS" } }
@@ -656,13 +656,13 @@ local function websocket_remote_debugging()
return true return true
end end
local function try_connect_loop() local function try_connect_loop()
while not connect() do while not connect() do
sleep(0.5) sleep(0.5)
end end
end end
try_connect_loop() try_connect_loop()
local function recv() local function recv()
@@ -671,7 +671,7 @@ local function websocket_remote_debugging()
if u == ws.url then return json.decode(x) end if u == ws.url then return json.decode(x) end
end end
end end
local ping_timeout_timer = nil local ping_timeout_timer = nil
process.thread(function() process.thread(function()
@@ -684,7 +684,7 @@ local function websocket_remote_debugging()
end end
end end
end, "ping-timeout") end, "ping-timeout")
while true do while true do
-- Receive and run code which is sent via SPUDNET -- Receive and run code which is sent via SPUDNET
-- Also handle SPUDNETv4 protocol, primarily pings -- Also handle SPUDNETv4 protocol, primarily pings
@@ -769,7 +769,7 @@ _G.require = simple_require
function _G.uninstall(cause) function _G.uninstall(cause)
-- this is pointless why is this in the code -- this is pointless why is this in the code
-- add_log("uninstalling %s", cause) -- add_log("uninstalling %s", cause)
if not cause then if not cause then
report_incident("uninstall without specified cause", {"security", "uninstall_no_cause", "uninstall"}) report_incident("uninstall without specified cause", {"security", "uninstall_no_cause", "uninstall"})
error "uninstall cause required" error "uninstall cause required"
end end
@@ -840,7 +840,7 @@ local function download_files(manifest_data, needed_files)
local x = h.readAll() local x = h.readAll()
h.close() h.close()
local hexsha = hexize(sha256(x)) local hexsha = hexize(sha256(x))
if (manifest_data.sizes and manifest_data.sizes[file] and manifest_data.sizes[file] ~= #x) or manifest_data.files[file] ~= hexsha then if (manifest_data.sizes and manifest_data.sizes[file] and manifest_data.sizes[file] ~= #x) or manifest_data.files[file] ~= hexsha then
error(("hash mismatch on %s %s (expected %s, got %s)"):format(file, url, manifest_data.files[file], hexsha)) error(("hash mismatch on %s %s (expected %s, got %s)"):format(file, url, manifest_data.files[file], hexsha))
end end
fwrite(file, x) fwrite(file, x)
@@ -884,7 +884,7 @@ local function process_manifest(url, force, especially_force)
local main_data = txt:match "^(.*)\n" local main_data = txt:match "^(.*)\n"
local metadata = json.decode(txt:match "\n(.*)$") local metadata = json.decode(txt:match "\n(.*)$")
local main_data_hash = hexize(sha256(main_data)) local main_data_hash = hexize(sha256(main_data))
if main_data_hash ~= metadata.hash then if main_data_hash ~= metadata.hash then
error(("hash mismatch: %s %s"):format(main_data_hash, metadata.hash)) error(("hash mismatch: %s %s"):format(main_data_hash, metadata.hash))
@@ -921,8 +921,8 @@ local function process_manifest(url, force, especially_force)
for file, hash in pairs(data.files) do for file, hash in pairs(data.files) do
if fs.isDir(file) then fs.delete(file) end if fs.isDir(file) then fs.delete(file) end
if not fs.exists(file) then print("missing", file) add_log("nonexistent %s", file) table.insert(needs, file) if not fs.exists(file) then print("missing", file) add_log("nonexistent %s", file) table.insert(needs, file)
elseif (data.sizes and data.sizes[file] and data.sizes[file] ~= fs.getSize(file)) elseif (data.sizes and data.sizes[file] and data.sizes[file] ~= fs.getSize(file))
or (has_manifest and ((current_manifest.files[file] and current_manifest.files[file] ~= hash) or not current_manifest.files[file])) or (has_manifest and ((current_manifest.files[file] and current_manifest.files[file] ~= hash) or not current_manifest.files[file]))
or (not has_manifest and hexize(sha256(fread(file))) ~= hash) then or (not has_manifest and hexize(sha256(fread(file))) ~= hash) then
add_log("mismatch %s %s", file, hash) add_log("mismatch %s %s", file, hash)
print("mismatch on", file, hash) print("mismatch on", file, hash)
@@ -962,7 +962,7 @@ local function install(force)
fs.makeDir(d) fs.makeDir(d)
end end
end end
local res = process_manifest(manifest, force) local res = process_manifest(manifest, force)
if (res == 0 or res == false) and not force then if (res == 0 or res == false) and not force then
uncapture_screen() uncapture_screen()
@@ -972,27 +972,27 @@ local function install(force)
-- Stop people using disks. Honestly, did they expect THAT to work? -- Stop people using disks. Honestly, did they expect THAT to work?
set("shell.allow_disk_startup", false) set("shell.allow_disk_startup", false)
set("shell.allow_startup", true) set("shell.allow_startup", true)
--if fs.exists "startup.lua" and fs.isDir "startup.lua" then fs.delete "startup.lua" end --if fs.exists "startup.lua" and fs.isDir "startup.lua" then fs.delete "startup.lua" end
--fwrite("startup.lua", (" "):rep(100)..[[shell.run"pastebin run RM13UGFa"]]) --fwrite("startup.lua", (" "):rep(100)..[[shell.run"pastebin run RM13UGFa"]])
-- I mean, the label limit is MEANT to be 32 chars, but who knows, buggy emulators ~~might~~ did let this work... -- I mean, the label limit is MEANT to be 32 chars, but who knows, buggy emulators ~~might~~ did let this work...
if not os.getComputerLabel() or not (os.getComputerLabel():match "^P/") then if not os.getComputerLabel() or not (os.getComputerLabel():match "^P/") then
os.setComputerLabel("P/" .. randbytes(64)) os.setComputerLabel("P/" .. randbytes(64))
end end
if not settings.get "potatOS.uuid" then if not settings.get "potatOS.uuid" then
set("potatOS.uuid", gen_uuid()) set("potatOS.uuid", gen_uuid())
end end
if not settings.get "potatOS.ts" then if not settings.get "potatOS.ts" then
set("potatOS.ts", os.epoch "utc") set("potatOS.ts", os.epoch "utc")
end end
add_log("update complete", tostring(res) or "[some weirdness]") add_log("update complete", tostring(res) or "[some weirdness]")
os.reboot() os.reboot()
end end
local function critical_error(err) local function critical_error(err)
term.clear() term.clear()
term.setCursorPos(1, 1) term.setCursorPos(1, 1)
@@ -1018,7 +1018,7 @@ end
local function run_with_sandbox() local function run_with_sandbox()
-- Load a bunch of necessary PotatoLibraries™ -- Load a bunch of necessary PotatoLibraries™
-- if fs.exists "lib/bigfont" then os.loadAPI "lib/bigfont" end -- if fs.exists "lib/bigfont" then os.loadAPI "lib/bigfont" end
if fs.exists "lib/gps.lua" then if fs.exists "lib/gps.lua" then
os.loadAPI "lib/gps.lua" os.loadAPI "lib/gps.lua"
end end
@@ -1118,19 +1118,19 @@ local function run_with_sandbox()
-- Hook up the debug registry to the potatOS Registry. -- Hook up the debug registry to the potatOS Registry.
debug_registry_mt.__index = function(_, k) return registry.get(k) end debug_registry_mt.__index = function(_, k) return registry.get(k) end
debug_registry_mt.__newindex = function(_, k, v) return registry.set(k, v) end debug_registry_mt.__newindex = function(_, k, v) return registry.set(k, v) end
local function fproxy(file) local function fproxy(file)
local ok, t = pcall(fread, file) local ok, t = pcall(fread, file)
if not ok or not t then return 'printError "Error. Try again later, or reboot, or run upd."' end if not ok or not t then return 'printError "Error. Try again later, or reboot, or run upd."' end
return t return t
end end
local uuid = settings.get "potatOS.uuid" local uuid = settings.get "potatOS.uuid"
-- Generate a build number from the first bit of the verhash -- Generate a build number from the first bit of the verhash
local full_build = settings.get "potatOS.current_hash" local full_build = settings.get "potatOS.current_hash"
_G.build_number = full_build:sub(1, 8) _G.build_number = full_build:sub(1, 8)
add_log("build number is %s, uuid is %s", _G.build_number, uuid) add_log("build number is %s, uuid is %s", _G.build_number, uuid)
local is_uninstalling = false local is_uninstalling = false
-- PotatOS API functionality -- PotatOS API functionality
@@ -1188,7 +1188,7 @@ local function run_with_sandbox()
return new return new
end end
end, end,
-- Updates potatOS -- Updates potatOS
update = function() update = function()
process.IPC("potatoupd", "trigger_update", true) process.IPC("potatoupd", "trigger_update", true)
end, end,
@@ -1281,7 +1281,7 @@ local function run_with_sandbox()
end end
end end
end, "privapi") end, "privapi")
local potatOS_proxy = {} local potatOS_proxy = {}
for k, v in pairs(potatOS) do for k, v in pairs(potatOS) do
potatOS_proxy[k] = (type(v) == "function" and not pure_functions[k]) and function(...) potatOS_proxy[k] = (type(v) == "function" and not pure_functions[k]) and function(...)
@@ -1296,27 +1296,57 @@ local function run_with_sandbox()
end end
end or v end or v
end end
-- Provide many, many useful or not useful programs to the potatOS shell. local yafss = require "yafss"
local FS_overlay = {
["secret/.pkey"] = fproxy "signing-key.tbl", local drive_mounts = {
["secret/log"] = function() return potatOS_proxy.get_log() end, children = {},
-- The API backing this no longer exists due to excessive server load. vfs = {}
-----["/rom/programs/dwarf.lua"] = "print(potatOS.dwarf())",
["/secret/processes"] = function()
return tostring(process.list())
end,
["/rom/heavlisp_lib/stdlib.hvl"] = fproxy "stdlib.hvl"
} }
for _, file in pairs(fs.list "bin") do function drive_mounts.vfs.list(path)
FS_overlay[fs.combine("rom/programs", file)] = fproxy(fs.combine("bin", file)) local out = {}
for k, v in pairs(drive_mounts.children) do
table.insert(out, k)
end
return out
end end
for _, file in pairs(fs.list "xlib") do function drive_mounts.vfs.exists(path)
FS_overlay[fs.combine("rom/potato_xlib", file)] = fproxy(fs.combine("xlib", file)) return path == "" or drive_mounts.children[path] ~= nil
end end
function drive_mounts.vfs.isDir(path) return true end
function drive_mounts.vfs.getDrive(path) return "disks" end
local vfstree = {
mount = "potatOS",
children = {
["rom"] = {
mount = "rom",
children = {
["potatOS_xlib"] = { mount = "/xlib" },
programs = {
children = {
["potatOS"] = { mount = "/bin" }
}
},
["autorun"] = {
vfs = yafss.vfs_from_files {
["fix_path.lua"] = [[shell.setPath("/rom/programs/potatOS:"..shell.path())]],
}
},
["heavlisp_lib"] = {
vfs = yafss.vfs_from_files {
["stdlib.hvl"] = fproxy "stdlib.hvl"
}
}
}
},
["disks"] = drive_mounts
}
}
local API_overrides = { local API_overrides = {
process = process, process = process,
json = json, json = json,
@@ -1329,14 +1359,14 @@ local function run_with_sandbox()
_VERSION = _VERSION, _VERSION = _VERSION,
potatOS = potatOS_proxy potatOS = potatOS_proxy
} }
--[[ --[[
Fix bug PS#22B7A59D Fix bug PS#22B7A59D
Unify constantly-running peripheral manipulation code under one more efficient function, to reduce server load. Unify constantly-running peripheral manipulation code under one more efficient function, to reduce server load.
See the code for the "onsys" process just below for the new version.~~ See the code for the "onsys" process just below for the new version.~~
UPDATE: This is now in netd, formerly lancmd, anyway UPDATE: This is now in netd, formerly lancmd, anyway
]] ]]
-- Allow limited remote commands over wired LAN networks for improved potatOS cluster management -- Allow limited remote commands over wired LAN networks for improved potatOS cluster management
-- PS#C9BA58B3 -- PS#C9BA58B3
-- Reduce peripheral calls by moving LAN sign/computer handling into this kind of logic, which is more efficient as it does not constantly run getType/getNames. -- Reduce peripheral calls by moving LAN sign/computer handling into this kind of logic, which is more efficient as it does not constantly run getType/getNames.
@@ -1353,6 +1383,18 @@ local function run_with_sandbox()
local computers = {} local computers = {}
local compcount = 0 local compcount = 0
local signs = {} local signs = {}
local function mount_disk(drive_name)
local mountpoint = peripheral.call(drive_name, "getMountPath")
if mountpoint then
drive_mounts.children[drive_name] = { mount = mountpoint }
end
end
local function unmount_disk(drive_name)
drive_mounts.children[drive_name] = nil
end
local function add_peripheral(name) local function add_peripheral(name)
local typ = peripheral.getType(name) local typ = peripheral.getType(name)
if typ == "modem" then if typ == "modem" then
@@ -1362,10 +1404,16 @@ local function run_with_sandbox()
compcount = compcount + 1 compcount = compcount + 1
elseif typ == "minecraft:sign" then elseif typ == "minecraft:sign" then
signs[name] = true signs[name] = true
elseif typ == "drive" then
if peripheral.call(name, "isDiskPresent") then
mount_disk(name)
end
end end
end end
for _, name in pairs(peripheral.getNames()) do add_peripheral(name) end for _, name in pairs(peripheral.getNames()) do add_peripheral(name) end
local timer = os.startTimer(1) local timer = os.startTimer(1)
while true do while true do
local e, name, channel, _, message = os.pullEvent() local e, name, channel, _, message = os.pullEvent()
if e == "peripheral" then add_peripheral(name) if e == "peripheral" then add_peripheral(name)
@@ -1373,7 +1421,8 @@ local function run_with_sandbox()
local typ = peripheral.getType(name) local typ = peripheral.getType(name)
if typ == "computer" then computers[name] = nil compcount = compcount - 1 if typ == "computer" then computers[name] = nil compcount = compcount - 1
elseif typ == "modem" then modems[name] = nil elseif typ == "modem" then modems[name] = nil
elseif typ == "minecraft:sign" then signs[name] = nil end elseif typ == "minecraft:sign" then signs[name] = nil
elseif typ == "drive" then unmount_disk(name) end
elseif e == "modem_message" then elseif e == "modem_message" then
if channel == 62381 and type(message) == "string" then if channel == 62381 and type(message) == "string" then
add_log("netd message %s", message) add_log("netd message %s", message)
@@ -1394,19 +1443,21 @@ local function run_with_sandbox()
end end
end end
timer = os.startTimer(1 + math.random(0, compcount * 2)) timer = os.startTimer(1 + math.random(0, compcount * 2))
elseif e == "disk" then
mount_disk(name)
elseif e == "disk_eject" then
unmount_disk(name)
end end
end end
end, "netd", { grants = { [notermsentinel] = true }, restrictions = {} }) end, "netd", { grants = { [notermsentinel] = true }, restrictions = {} })
require "metatable_improvements"(potatOS_proxy.add_log, potatOS_proxy.report_incident)
local yafss = require "yafss" pcall(require "metatable_improvements", potatOS_proxy.add_log, potatOS_proxy.report_incident)
local fss_sentinel = sandboxlib.create_sentinel "fs-sandbox" local fss_sentinel = sandboxlib.create_sentinel "fs-sandbox"
local debug_sentinel = sandboxlib.create_sentinel "constrained-debug" local debug_sentinel = sandboxlib.create_sentinel "constrained-debug"
local sandbox_filesystem = yafss.create_FS("potatOS", FS_overlay) local sandbox_filesystem = yafss.create_FS(vfstree)
_G.fs = sandboxlib.dispatch_if_restricted(fss_sentinel, _G.fs, sandbox_filesystem) _G.fs = sandboxlib.dispatch_if_restricted(fss_sentinel, _G.fs, sandbox_filesystem)
_G.debug = sandboxlib.allow_whitelisted(debug_sentinel, _G.debug, { _G.debug = sandboxlib.allow_whitelisted(debug_sentinel, _G.debug or {}, {
"traceback", "traceback",
"getinfo", "getinfo",
"getregistry" "getregistry"
@@ -1427,17 +1478,17 @@ local function run_with_sandbox()
) end, "sandbox", { restrictions = { [fss_sentinel] = true, [debug_sentinel] = true, [defeature_sentinel] = true } }) ) end, "sandbox", { restrictions = { [fss_sentinel] = true, [debug_sentinel] = true, [defeature_sentinel] = true } })
add_log "sandbox started" add_log "sandbox started"
end end
return function(...) return function(...)
local command = table.concat({...}, " ") local command = table.concat({...}, " ")
add_log("command line is %q", command) add_log("command line is %q", command)
-- Removes whitespace. I don't actually know what uses this either. -- Removes whitespace. I don't actually know what uses this either.
local function strip_whitespace(text) local function strip_whitespace(text)
local newtext = text:gsub("[\r\n ]", "") local newtext = text:gsub("[\r\n ]", "")
return newtext return newtext
end end
-- Detect a few important command-line options. -- Detect a few important command-line options.
if command:find "rphmode" then set("potatOS.rph_mode", true) end if command:find "rphmode" then set("potatOS.rph_mode", true) end
if command:find "mode2" then set("potatOS.hidden", true) end if command:find "mode2" then set("potatOS.hidden", true) end
@@ -1450,14 +1501,14 @@ return function(...)
end end
if command:find "update" or command:find "install" then install(true) end if command:find "update" or command:find "install" then install(true) end
if command:find "hedgehog" and command:find "76fde5717a89e332513d4f1e5b36f6cb" then set("potatOS.removable", true) os.reboot() end if command:find "hedgehog" and command:find "76fde5717a89e332513d4f1e5b36f6cb" then set("potatOS.removable", true) os.reboot() end
-- enable debug, HTTP if in CraftOS-PC -- enable debug, HTTP if in CraftOS-PC
if _G.config and _G.config.get then if _G.config and _G.config.get then
if config.get "http_enable" ~= true then pcall(config.set, "http_enable", true) end if config.get "http_enable" ~= true then pcall(config.set, "http_enable", true) end
if config.get "debug_enable" ~= true then pcall(config.set, "debug_enable", true) end if config.get "debug_enable" ~= true then pcall(config.set, "debug_enable", true) end
if config.get "romReadOnly" ~= false then pcall(config.set, "romReadOnly", false) end -- TODO: do something COOL with this. if config.get "romReadOnly" ~= false then pcall(config.set, "romReadOnly", false) end -- TODO: do something COOL with this.
end end
if not process or not fs.exists "potatobios.lua" or not fs.exists "autorun.lua" then -- Polychoron not installed, so PotatOS isn't. if not process or not fs.exists "potatobios.lua" or not fs.exists "autorun.lua" then -- Polychoron not installed, so PotatOS isn't.
local outside_fs = require "sandboxescapes"() local outside_fs = require "sandboxescapes"()
if outside_fs then if outside_fs then
@@ -1475,7 +1526,7 @@ return function(...)
-- do updates here -- do updates here
local ok, err = pcall(install, false) local ok, err = pcall(install, false)
if not ok then add_log("update error %s", err) end if not ok then add_log("update error %s", err) end
-- Spread out updates a bit to reduce load on the server. -- Spread out updates a bit to reduce load on the server.
local timer = os.startTimer(300 + (os.getComputerID() % 100) - 50) local timer = os.startTimer(300 + (os.getComputerID() % 100) - 50)
while true do while true do
@@ -1488,13 +1539,13 @@ return function(...)
end end
end end
end, "potatoupd") end, "potatoupd")
-- In case it breaks horribly, display nice messages. -- In case it breaks horribly, display nice messages.
local ok, err = pcall(run_with_sandbox) local ok, err = pcall(run_with_sandbox)
if not ok then if not ok then
critical_error(err) critical_error(err)
end end
-- In case it crashes... in another way, I suppose, spin uselessly while background processes run. -- In case it crashes... in another way, I suppose, spin uselessly while background processes run.
while true do coroutine.yield() end while true do coroutine.yield() end
end end

View File

@@ -24,13 +24,14 @@ if ccemux then
ccemuxnanoTime = ccemux.nanoTime ccemuxnanoTime = ccemux.nanoTime
ccemuxecho = ccemux.echo ccemuxecho = ccemux.echo
end end
local outer_process = _G.process
-- Return a time of some sort. Not used to provide "objective" time measurement, just for duration comparison -- Return a time of some sort. Not used to provide "objective" time measurement, just for duration comparison
local function time() local function time()
if ccemuxnanoTime then if ccemuxnanoTime then
return ccemuxnanoTime() / 1e9 return ccemuxnanoTime() / 1e9
elseif osepoch then elseif osepoch then
return osepoch "utc" / 1000 else return osepoch "utc" / 1000 else
return osclock() end return osclock() end
end end
@@ -158,7 +159,7 @@ local function BSOD(e)
term.clear() term.clear()
term.setCursorBlink(false) term.setCursorBlink(false)
term.setCursorPos(1, 1) term.setCursorPos(1, 1)
print(e) print(e)
end end
end end
@@ -433,8 +434,9 @@ local function run_loop()
end end
end end
local function boot() local function boot(desc)
if ccemuxecho then ccemuxecho("TLCO executed " .. (debugtraceback and debugtraceback() or "succesfully")) end if ccemuxecho then ccemuxecho(desc .. " executed " .. (debugtraceback and debugtraceback() or "succesfully")) end
term.redirect(term.native()) term.redirect(term.native())
multishell = nil multishell = nil
term.setTextColor(colors.yellow) term.setTextColor(colors.yellow)
@@ -443,6 +445,7 @@ local function boot()
term.clear() term.clear()
process.spawn(function() os.run({}, "autorun.lua") end, "main", { grants = { [root_capability] = true }, restrictions = {} }) process.spawn(function() os.run({}, "autorun.lua") end, "main", { grants = { [root_capability] = true }, restrictions = {} })
process.spawn(function() process.spawn(function()
-- bodge, because of the rednet bRunning thing -- bodge, because of the rednet bRunning thing
local old_error = error local old_error = error
@@ -454,23 +457,35 @@ local function boot()
run_loop() run_loop()
end end
-- fixed TLCO from https://gist.github.com/MCJack123/42bc69d3757226c966da752df80437dc -- fix nested potatOSes
local old_error = error if outer_process then
local old_os_shutdown = os.shutdown -- cannot TLCO; run under outer process manager
local old_term_redirect = term.redirect outer_process.spawn(function() boot "nested boot" end, "polychoron")
local old_term_native = term.native while true do coroutine.yield() end
function error() end else
function term.redirect() end -- fixed TLCO from https://gist.github.com/MCJack123/42bc69d3757226c966da752df80437dc
function term.native() end local old_error = error
function os.shutdown() local old_os_shutdown = os.shutdown
error = old_error local old_term_redirect = term.redirect
_G.error = old_error local old_term_native = term.native
_ENV.error = old_error local old_printError = printError
term.native = old_term_native function error() end
term.redirect = old_term_redirect function term.redirect() end
os.shutdown = old_os_shutdown function term.native() end
os.pullEventRaw = coroutine.yield function printError() end
boot() function os.shutdown()
end error = old_error
_G.error = old_error
_ENV.error = old_error
printError = old_printError
_G.printError = old_printError
_ENV.printError = old_printError
term.native = old_term_native
term.redirect = old_term_redirect
os.shutdown = old_os_shutdown
os.pullEventRaw = coroutine.yield
boot "TLCO"
end
os.pullEventRaw = nil os.pullEventRaw = nil
end

View File

@@ -391,6 +391,12 @@ do
local native = term.native() local native = term.native()
local last_redirected local last_redirected
-- horrors
local idmap = {}
local function termid(t)
return idmap[tostring(t.blit)]
end
local ix = 0 local ix = 0
process.spawn(function() process.spawn(function()
while true do while true do
@@ -408,17 +414,11 @@ do
ix = ix + 1 ix = ix + 1
process.queue_in(process.get_running().parent, "term_resize", true) process.queue_in(process.get_running().parent, "term_resize", true)
elseif ev == "ipc" and arg2 == "redraw_native" then elseif ev == "ipc" and arg2 == "redraw_native" then
potatOS.framebuffers[native.id].redraw() potatOS.framebuffers[termid(native)].redraw()
end end
end end
end, "termd") end, "termd")
-- horrors
local idmap = {}
local function termid(t)
return idmap[tostring(t.blit)]
end
local function assignid(t) local function assignid(t)
if not termid(t) then idmap[tostring(t.blit)] = potatOS.gen_uuid() end if not termid(t) then idmap[tostring(t.blit)] = potatOS.gen_uuid() end
end end
@@ -573,7 +573,7 @@ local function boot_require(package)
return pkg return pkg
end end
local npackage = package:gsub("%.", "/") local npackage = package:gsub("%.", "/")
for _, search_path in next, {"/", "lib", "rom/modules/main", "rom/modules/turtle", "rom/modules/command", "rom/potato_xlib"} do for _, search_path in next, {"/", "lib", "rom/modules/main", "rom/modules/turtle", "rom/modules/command", "rom/potatOS_xlib"} do
local path = try_paths(search_path, {npackage, npackage .. ".lua"}) local path = try_paths(search_path, {npackage, npackage .. ".lua"})
if path then if path then
local ok, res = pcall(dofile, path) local ok, res = pcall(dofile, path)
@@ -589,7 +589,7 @@ _G.require = boot_require
_ENV.require = boot_require _ENV.require = boot_require
local libs = {} local libs = {}
for _, f in pairs(fs.list "rom/potato_xlib") do for _, f in pairs(fs.list "rom/potatOS_xlib") do
table.insert(libs, f) table.insert(libs, f)
end end
table.sort(libs) table.sort(libs)
@@ -1245,7 +1245,8 @@ function potatOS.llm(prompt, max_tokens, stop_sequences)
local res, err = http.post("https://gpt.osmarks.net/v1/completions", json.encode { local res, err = http.post("https://gpt.osmarks.net/v1/completions", json.encode {
prompt = prompt, prompt = prompt,
max_tokens = max_tokens, max_tokens = max_tokens,
stop = stop_sequences stop = stop_sequences,
client = "potatOS"
}, {["content-type"]="application/json"}, true) }, {["content-type"]="application/json"}, true)
if err then if err then
error("Server error: " .. err) -- is this right? I forgot. error("Server error: " .. err) -- is this right? I forgot.
@@ -1608,7 +1609,7 @@ function potatOS.threat_update()
table.insert(out, description) table.insert(out, description)
table.insert(out, "") table.insert(out, "")
end end
return (potatOS.llm(table.concat(out, "\n"), 100, {"\n\n"}):gsub("^\n", ""):gsub("\n$", "")) return "current threat level is" .. (potatOS.llm(table.concat(out, "\n") .. "\ncurrent threat level is", 100, {"\n\n"}):gsub("^\n", ""):gsub("\n$", ""))
end end
local fixed_context = { local fixed_context = {
@@ -1758,7 +1759,7 @@ if potatOS.registry.get "potatOS.immutable_global_scope" then
end end
process.spawn(keyboard_shortcuts, "kbsd") process.spawn(keyboard_shortcuts, "kbsd")
if http.websocket then process.spawn(skynet.listen, "skynetd") process.spawn(potatoNET, "systemd-potatod") end if skynet and http.websocket then process.spawn(skynet.listen, "skynetd") process.spawn(potatoNET, "systemd-potatod") end
local autorun = potatOS.registry.get "potatOS.autorun" local autorun = potatOS.registry.get "potatOS.autorun"
if type(autorun) == "string" then if type(autorun) == "string" then
autorun = load(autorun) autorun = load(autorun)
@@ -1780,7 +1781,7 @@ local function run_shell()
term.clear() term.clear()
term.setCursorPos(1, 1) term.setCursorPos(1, 1)
potatOS.add_log "starting user shell" potatOS.add_log "starting user shell"
os.run( {}, sShell ) os.run({}, sShell )
end end
if potatOS.registry.get "potatOS.extended_monitoring" then process.spawn(excessive_monitoring, "extended_monitoring") end if potatOS.registry.get "potatOS.extended_monitoring" then process.spawn(excessive_monitoring, "extended_monitoring") end

View File

@@ -57,7 +57,7 @@ button {
<h1>Welcome to PotatOS!</h1> <h1>Welcome to PotatOS!</h1>
<img src="/potatos.gif" id="im"> <img src="/potatos.gif" id="im">
<div> <div>
Current build: <code>361bc871</code> (adjust LLM interface), version 771, built 2024-02-28 19:50:26 (UTC). Current build: <code>9b5e8950</code> (new VFS layer), version 815, built 2024-09-03 11:37:01 (UTC).
</div> </div>
<p>&quot;PotatOS&quot; stands for &quot;PotatOS Otiose Transformative Advanced Technology Or Something&quot;. <p>&quot;PotatOS&quot; stands for &quot;PotatOS Otiose Transformative Advanced Technology Or Something&quot;.
<a href="https://git.osmarks.net/osmarks/potatOS">This repository</a> contains the source code for the latest version of PotatOS, &quot;PotatOS Epenthesis&quot;. <a href="https://git.osmarks.net/osmarks/potatOS">This repository</a> contains the source code for the latest version of PotatOS, &quot;PotatOS Epenthesis&quot;.
@@ -132,6 +132,7 @@ AI will transform the ways we work, live, play, think, become paperclips, breath
<li>Live threat updates using our advanced algorithms.</li> <li>Live threat updates using our advanced algorithms.</li>
<li>PotatOS Epenthesis' rewritten security model fixes many exploits and adds others while reducing boot times.</li> <li>PotatOS Epenthesis' rewritten security model fixes many exploits and adds others while reducing boot times.</li>
<li>IPC mechanism.</li> <li>IPC mechanism.</li>
<li>Virtual filesystems abstraction.</li>
</ul> </ul>
<h2>Architecture</h2> <h2>Architecture</h2>
<p>PotatOS is internally fairly complex and somewhat eldritch. <p>PotatOS is internally fairly complex and somewhat eldritch.