Delay perlin noise initialization until actually used

This commit is contained in:
DBotThePony 2024-03-30 18:15:06 +07:00
parent fd1a63a22c
commit ff746f43ae
Signed by: DBot
GPG Key ID: DCC23B5715498507
11 changed files with 61 additions and 94 deletions

View File

@ -6,7 +6,7 @@
### Worldgen
* Where applicable, Perlin noise now can have custom seed specified
* Change above allows to explicitly specify universe seed (as `celestial.config:systemTypePerlin:seed`)
* Perlin noise now can be of arbitrary scale (defaults to `512`, specified with `scale` key, integer type, >=16)
* Perlin noise now can be of arbitrary scale (defaults to `512`, specified with `scale` key, integer type, 2048>=x>=16)
#### Terrain
* `mix` terrain selector got `mixSeedBias`, `aSeedBias` and `bSeedBias` fields, whose deviate respective selectors seeds (default to `0`)

View File

@ -1,68 +0,0 @@
## Vulnerabilities in original engine
This document points out vulnerabilities in original game engine and describes vectors of attack
to exploit them.
This document is for educational purposes only to raise awareness (about learning how dangerous it is to run public Starbound
server on original engine), and pursues no goal of harming users of original engine.
Experienced blackhats already could take sources and dig these invulnerabilities themselves,
since most of them are not buried anywhere deep in code.
-----------
### EntityDestroyPacket vulnerability
When client sends EntityCreatePacket to WorldServer, it checks whenever received `entityId` is within
allowed range (range of IDs allocated specifically for that client). Same happens on EntityUpdateSetPacket.
However, someone forgot to put the same check when receiving EntityDestroyPacket, hence
any client can remove ANY other entity inside world, including other PlayerEntitys'.
On side note, original client makes sure it sends EntityDestroyPacket only for entities it owns.
This attack require modified game client.
-----------
### Zip bomb in PacketSocket
When packets are received on network socket, they are checked for not exceeding 16 MiB,
by reading packet length header. However, when receiving compressed packets,
only compressed size is checked against 16 MiB limit, and
they are uncompressed in one shot, without limiting uncompressed size.
This vulnerability allows to make server quickly run out of memory by forging zip-bomb packet.
This attack require modified game client.
-----------
### Client's ShipWorld size
When joining server, client sends contents of `.shipworld` in form of chunk map
(Map with bytearray keys and bytearray values, which represent data stored inside BTreeDB).
Server instances WorldServer with provided world chunks. The vulnerability lies within world's size.
Original engine world's chunk map is always stored as tight 2D array of chunk (sector) pointers,
and pointer array is always fully preallocated when world is instanced.
So client can forge custom shipworld, with 2^31 x 2^31 dimensions, which will instantly cause
server to consume at least 128 GiB of RAM when client connects.
This attack does not require modified game client.
-----------
## Exploits in original engine
These kind of bugs don't directly compromise security of server, but may degrade its performance.
-----------
### Client context window size
Window size as reported by client is not checked for insane values, allowing to greatly slowdown the server if client
is residing in large world, and reporting to be tracking entire world.

View File

@ -19,7 +19,8 @@ data class PerlinNoiseParameters(
val bias: Double = 0.0,
) {
init {
require(scale >= 16) { "Too little perlin noise scale" }
require(scale >= 16) { "Too little perlin noise scale: $scale" }
require(scale <= 2048) { "Absurd noise scale: $scale" }
}
enum class Type(override val jsonName: String) : IStringSerializable {

View File

@ -188,7 +188,7 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
baseInformation = Starbound.gson.fromJson(stream0)!!
generationInformation = Starbound.gson.fromJson(stream1)!!
if (!generationInformation.systemTypePerlin.isInitialized)
if (!generationInformation.systemTypePerlin.hasSeedSpecified)
generationInformation.systemTypePerlin.init(staticRandom64("SystemTypePerlin"))
celestialNames = Starbound.gson.fromJson(Starbound.jsonReader("/celestial/names.config"))!!

View File

@ -229,7 +229,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
data,
entity.entityID
))
} else {
} else if (entity.networkGroup.upstream.hasChangedSince(entityVersions.get(id))) {
val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy)
entityVersions.put(id, version)
send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data)))

View File

@ -27,9 +27,12 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
abstract operator fun get(x: Double, y: Double): Double
abstract operator fun get(x: Double, y: Double, z: Double): Double
var isInitialized = false
var hasSeedSpecified = false
private set
private var isInitialized = false
private val initLock = Any()
var seed: Long = 0L
private set
@ -48,13 +51,7 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
}
}
fun init(seed: Long) {
if (parameters.type == PerlinNoiseParameters.Type.UNITIALIZED)
return
isInitialized = true
this.seed = seed
private fun doInit(seed: Long) {
p.fill(0)
g1.fill(0.0)
g2.fill(0.0)
@ -116,6 +113,30 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
}
}
protected fun checkInit() {
synchronized(initLock) {
check(hasSeedSpecified) { "Tried to use perlin noise without seed specified" }
if (!isInitialized) {
doInit(seed)
isInitialized = true
}
}
}
fun init(seed: Long, now: Boolean = false) {
if (parameters.type == PerlinNoiseParameters.Type.UNITIALIZED)
return
this.hasSeedSpecified = true
this.isInitialized = false
this.seed = seed
if (now) {
checkInit()
}
}
protected fun curve(value: Double): Double {
return value * value * (3.0 - 2.0 * value)
}

View File

@ -9,6 +9,7 @@ class BillowNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(param
}
override fun get(x: Double): Double {
checkInit()
var sum = 0.0
var p = x * parameters.frequency
var scale = 1.0
@ -23,6 +24,7 @@ class BillowNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(param
}
override fun get(x: Double, y: Double): Double {
checkInit()
var sum = 0.0
var px = x * parameters.frequency
var py = y * parameters.frequency
@ -39,6 +41,7 @@ class BillowNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(param
}
override fun get(x: Double, y: Double, z: Double): Double {
checkInit()
var sum = 0.0
var px = x * parameters.frequency
var py = y * parameters.frequency

View File

@ -8,6 +8,7 @@ class PerlinNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(param
}
override fun get(x: Double): Double {
checkInit()
var sum = 0.0
var p = x * parameters.frequency
var scale = 1.0
@ -22,6 +23,7 @@ class PerlinNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(param
}
override fun get(x: Double, y: Double): Double {
checkInit()
var sum = 0.0
var px = x * parameters.frequency
var py = y * parameters.frequency
@ -38,6 +40,7 @@ class PerlinNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(param
}
override fun get(x: Double, y: Double, z: Double): Double {
checkInit()
var sum = 0.0
var px = x * parameters.frequency
var py = y * parameters.frequency

View File

@ -9,6 +9,7 @@ class RidgedNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(param
}
override fun get(x: Double): Double {
checkInit()
var sum = 0.0
var p = x * parameters.frequency
var scale = 1.0
@ -31,6 +32,7 @@ class RidgedNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(param
}
override fun get(x: Double, y: Double): Double {
checkInit()
var sum = 0.0
var px = x * parameters.frequency
var py = y * parameters.frequency
@ -55,6 +57,7 @@ class RidgedNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(param
}
override fun get(x: Double, y: Double, z: Double): Double {
checkInit()
var sum = 0.0
var px = x * parameters.frequency
var py = y * parameters.frequency

View File

@ -21,7 +21,7 @@ import kotlin.concurrent.withLock
// while also setting up ground for better spatial index strategies, if they
// have to be done in the future.
class SpatialIndex<T>(val geometry: WorldGeometry) {
private val lock = ReentrantLock()
private val lock = Any()
private val map = Long2ObjectOpenHashMap<Sector>()
private val factory = Long2ObjectFunction { Sector(it) }
private val counter = AtomicInteger()
@ -97,7 +97,7 @@ class SpatialIndex<T>(val geometry: WorldGeometry) {
inner class Fixture {
init {
lock.withLock {
synchronized(lock) {
fixtures.add(this)
}
}
@ -137,7 +137,7 @@ class SpatialIndex<T>(val geometry: WorldGeometry) {
val newSectors0 = newSectors.toLongArray()
newSectors0.sort()
lock.withLock {
synchronized(lock) {
if (isRemoved) return
val addSectors = ArrayList<Sector>(0)
@ -217,25 +217,30 @@ class SpatialIndex<T>(val geometry: WorldGeometry) {
}
}
internal fun clear0() {
sectors.forEach { deref(it) }
sectors.clear()
}
fun clear() {
lock.withLock {
sectors.forEach { deref(it) }
sectors.clear()
synchronized(lock) {
clear0()
}
}
fun remove() {
lock.withLock {
synchronized(lock) {
isRemoved = true
clear()
sectors.forEach { deref(it) }
sectors.clear()
fixtures.remove(this)
}
}
}
fun remove() {
lock.withLock {
fixtures.forEach { it.clear() }
synchronized(lock) {
fixtures.forEach { it.clear0() }
sectors.keys.forEach { it.remove(this) }
sectors.clear()
}

View File

@ -9,7 +9,6 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.cos
import kotlin.math.sin
class IslandSurfaceTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector<IslandSurfaceTerrainSelector.Data>(data, parameters) {
@ -40,7 +39,7 @@ class IslandSurfaceTerrainSelector(data: Data, parameters: TerrainSelectorParame
val islandHeight by lazy {
val perlin = AbstractPerlinNoise.of(this.data.islandHeight)
if (!perlin.isInitialized) {
if (!perlin.hasSeedSpecified) {
perlin.init(islandHeightSeed)
}
@ -50,7 +49,7 @@ class IslandSurfaceTerrainSelector(data: Data, parameters: TerrainSelectorParame
val islandDepth by lazy {
val perlin = AbstractPerlinNoise.of(this.data.islandDepth)
if (!perlin.isInitialized) {
if (!perlin.hasSeedSpecified) {
perlin.init(islandDepthSeed)
}
@ -60,7 +59,7 @@ class IslandSurfaceTerrainSelector(data: Data, parameters: TerrainSelectorParame
val islandDecision by lazy {
val perlin = AbstractPerlinNoise.of(this.data.islandDecision)
if (!perlin.isInitialized) {
if (!perlin.hasSeedSpecified) {
perlin.init(islandDecisionSeed)
}