Challenge
Sam was trying to make a program to show how 1337 he is. But the
output isn't always correct. Help him fix his program so he can impress
his friends.
Sam 写了个程序想显得自己很 1337,但输出总是不对;帮他把程序修好。
关卡页给出完整的类源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package org.hackthissite.missions.extbasic;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ExtBasic14 { private final ExecutorService executorService = Executors.newFixedThreadPool(100 ); private static final int MAX = 1337 ; private int timeToGetLeet = 0 ; ExtBasic14() throws InterruptedException { for (int i = 0 ; i < MAX; i++) { executorService.execute(new Runnable () { public void run () { incrementLeetness(); } }); } executorService.shutdown(); while (!executorService.isTerminated()) { Thread.sleep(500 ); } System.out.println(timeToGetLeet); } private void incrementLeetness () { int obfusticatedIncremental = timeToGetLeet; obfusticatedIncremental = obfusticatedIncremental + 1 ; timeToGetLeet = obfusticatedIncremental; } public static void main (String[] args) throws InterruptedException { new ExtBasic14 (); } }
Solution
MAX = 1337,构造函数把 1337 个任务提交进一个 100
线程的固定池,每个任务只调用一次
incrementLeetness(),shutdown 等池终止后打印
timeToGetLeet。程序想要的输出是 1337。
被点名的 incrementLeetness() 是一个三步的读-改-写:
1 2 3 4 5 private void incrementLeetness () { int obfusticatedIncremental = timeToGetLeet; obfusticatedIncremental = obfusticatedIncremental + 1 ; timeToGetLeet = obfusticatedIncremental; }
而且 timeToGetLeet 是普通实例字段,没有
volatile,整个方法也没有任何锁。
多线程交错执行这段代码时会出现经典的丢失更新:
1 2 3 4 5 6 线程 A: obfusticatedIncremental = timeToGetLeet // 读到 0 线程 B: obfusticatedIncremental = timeToGetLeet // 也读到 0 线程 A: obfusticatedIncremental = 0 + 1 线程 A: timeToGetLeet = 1 线程 B: obfusticatedIncremental = 0 + 1 线程 B: timeToGetLeet = 1
A、B 各做了一次自增,计数却只从 0 走到 1:B 读到的旧值 0 在 A
写回之后仍然被写回,A 的那次增量被覆盖。100
个线程争抢同一个三段序列,丢失的更新累积起来,System.out.println
打出的值就稳定地低于 1337。
用 OpenJDK 17.0.20.1:原始类原样编为
broken,只把方法声明改成
private synchronized void 编为 fixed,各跑 10
次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 $ javac -d broken/out broken/org/hackthissite/missions/extbasic/ExtBasic14.java $ javac -d fixed/out fixed/org/hackthissite/missions/extbasic/ExtBasic14.java $ for i in $(seq 1 10); do java -cp broken/out org.hackthissite.missions.extbasic.ExtBasic14; done 1333 1336 1332 1335 1337 1335 1335 1335 1337 1335 $ for i in $(seq 1 10); do java -cp fixed/out org.hackthissite.missions.extbasic.ExtBasic14; done 1337 1337 1337 1337 1337 1337 1337 1337 1337 1337
broken 十次落在 1332–1337
之间(只有两次偶然凑满),fixed 十次全是
1337。两份源码的唯一差异是那个 synchronized:
1 2 3 4 5 private synchronized void incrementLeetness () { int obfusticatedIncremental = timeToGetLeet; obfusticatedIncremental = obfusticatedIncremental + 1 ; timeToGetLeet = obfusticatedIncremental; }
synchronized 加在实例方法上,等价于整段方法体在
this
的监视器锁内执行:同一时刻只有一个线程能进入读-改-写序列,丢失的更新随之消失。
playit 的 pass
字段比对的是修正后的整行方法声明 ,大小写敏感:
1 private synchronized void incrementLeetness () {
formkey
每次加载关卡页都会变,取页面和提交必须在同一次运行里完成;提交必须带
Referer: <关卡页>,否则模板页按无效 referer
丢弃。会话 cookie 从环境变量 HTS_COOKIE 读入,不落盘:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 """HackThisSite Extended Basic 14 (playit) live solver. The level ships a Java class whose counter is incremented from 1337 tasks on a 100-thread pool. ``timeToGetLeet`` is a plain (non-volatile) instance field and ``incrementLeetness`` is a non-atomic read-modify-write, so concurrent workers lose updates and the printed counter lands below 1337. The pinned answer is the corrected method declaration, submitted verbatim. Usage: export HTS_COOKIE='HackThisSite=<mission-cookie>' cd <hts-workspace>/challenges/hts-playit && uv run python extbasic-14/solve.py """ import osimport reimport urllib.parseimport urllib.requestUA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/131.0.0.0 Safari/537.36" ) BASE = "https://www.hackthissite.org" LEVEL = BASE + "/missions/playit/extbasic/14/" SUBMIT = BASE + "/missions/extbasic/template.php" ANSWER = "private synchronized void incrementLeetness() {" COOKIE = os.environ["HTS_COOKIE" ] OUT = os.path.dirname(os.path.abspath(__file__)) def fetch (url ): req = urllib.request.Request(url, headers={"Cookie" : COOKIE, "User-Agent" : UA}) return urllib.request.urlopen(req, timeout=30 ).read().decode("utf-8" , "replace" ) def main (): page = fetch(LEVEL) formkey = re.search(r'name="formkey" value="([^"]+)"' , page).group(1 ) lvl = re.search(r'name="lvl" value="([^"]+)"' , page).group(1 ) body = urllib.parse.urlencode( {"formkey" : formkey, "lvl" : lvl, "pass" : ANSWER}).encode() req = urllib.request.Request( SUBMIT, data=body, headers={"Cookie" : COOKIE, "User-Agent" : UA, "Content-Type" : "application/x-www-form-urlencoded" , "Referer" : LEVEL}) resp = urllib.request.urlopen(req, timeout=30 ).read().decode("utf-8" , "replace" ) open (os.path.join(OUT, "submit_resp.html" ), "w" , encoding="utf-8" ).write(resp) print ("formkey=%s lvl=%s" % (formkey, lvl)) print ("submit response bytes:" , len (resp)) if "Congratz" in resp: print ("[+] accepted - the category is reported complete" ) elif "/missions/playit/extbasic/14" in resp: print ("[+] accepted - go-on marker present in response" ) if __name__ == "__main__" : main()
运行:
1 2 3 4 5 $ export HTS_COOKIE='HackThisSite=<mission-cookie>' $ cd <hts-workspace>/challenges/hts-playit && uv run python extbasic-14/solve.py formkey=EkQSIZ58VEAE5WHfCkHKKx5xJkZtg5RtauGvVojU lvl=14 submit response bytes: 24074 [+] accepted - the category is reported complete
14 是本分类的最后一关,接受后提交响应里带总结语:
1 2 These are the _extended_ basics. Congratz! You have completed all the missions in this category
Key points
递增(x = x + 1)不是原子操作,多线程下必须用锁或原子类型保护;synchronized
方法等价于用 this 做互斥。
只加 volatile
不够:它保证可见性与有序性,但不会让读-改-写变成原子操作,两个线程仍能交错读走同一个旧值。
AtomicInteger.incrementAndGet() 用 CAS
免锁达到同样的原子性;本例只需修 incrementLeetness,加
synchronized 是最小改动。
竞争窗口越小越难复现:broken 十次里有两次恰好是
1337,判断修好没有要多跑几次,不能只看单次输出。
private synchronized void incrementLeetness() {